/** Copyright (C) 2009 by Aleksey Surkov.
 **
 ** Permission to use, copy, modify, and distribute this software and its
 ** documentation for any purpose and without fee is hereby granted, provided
 ** that the above copyright notice appear in all copies and that both that
 ** copyright notice and this permission notice appear in supporting
 ** documentation.  This software is provided "as is" without express or
 ** implied warranty.
 */

package de.siro.notie_surface;

import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder.AudioSource;
import android.os.Handler;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import de.siro.notie_surface.MainActivity.SurfaceThread;

public class PitchDetector implements Runnable {

    private final static int RATE = 8000;
    private final static int CHANNEL_MODE = AudioFormat.CHANNEL_IN_MONO;
	private final static int ENCODING = AudioFormat.ENCODING_PCM_16BIT;

	private final static int BUFFER_SIZE_IN_MS = 3000;
	private final static int CHUNK_SIZE_IN_SAMPLES_POW2 = 11;
	private final static int CHUNK_SIZE_IN_SAMPLES = 2048; // = 2 ^
															// CHUNK_SIZE_IN_SAMPLES_POW2
	private final static int CHUNK_SIZE_IN_MS = 1000 * CHUNK_SIZE_IN_SAMPLES
			/ RATE;
	private final static int BUFFER_SIZE_IN_BYTES = RATE * BUFFER_SIZE_IN_MS
			/ 1000 * 2;
	private final static int CHUNK_SIZE_IN_BYTES = RATE * CHUNK_SIZE_IN_MS
			/ 1000 * 2;
	private final static int MIN_FREQUENCY = 50; // HZ
    private final static int MAX_FREQUENCY = 4000; // HZ
    private final static int DRAW_FREQUENCY_STEP = 10;
    private final static int MIN_AMPLITUDE = 5000;
    FFT fft = new FFT(CHUNK_SIZE_IN_SAMPLES_POW2);

	public PitchDetector(SurfaceThread parent) {
		parent_ = parent;
		handler_ = new Handler();
	}

	private static class FreqResult {
		public HashMap<Double, Double> frequencies;
		public double best_frequency;
	}

	public static class FrequencyCluster {
		public double average_frequency = 0;
		public double total_amplitude = 0;

		public void add(double freq, double amplitude) {
			double new_total_amp = total_amplitude + amplitude;
			average_frequency = (total_amplitude * average_frequency + freq
					* amplitude)
					/ new_total_amp;
			total_amplitude = new_total_amp;
		}

		public boolean isNear(double freq) {
			if (Math.abs(1 - (average_frequency / freq)) < 0.05) {
				// only 5% difference
				return true;
			} else {
				return false;
			}
		}

		public boolean isHarmonic(double freq) {
			double harmonic_factor = freq / average_frequency;
			double distance_from_int = Math.abs(Math.round(harmonic_factor)
					- harmonic_factor);
			if (distance_from_int < 0.05) {
				// only 5% distance
				return true;
			} else {
				return false;
			}
		}

		public void addHarmony(double freq, double amp) {
			total_amplitude += amp;
		}

		@Override
		public String toString() {
			return "(" + average_frequency + ", " + total_amplitude + ")";
		}
	}

	public void run() {
		android.os.Process
				.setThreadPriority(android.os.Process.THREAD_PRIORITY_URGENT_AUDIO);
		recorder_ = new AudioRecord(AudioSource.MIC, RATE, CHANNEL_MODE,
				ENCODING, 6144);
		if (recorder_.getState() != AudioRecord.STATE_INITIALIZED) {
			return;
		}

		final int min_frequency_fft = Math.round(MIN_FREQUENCY
				* CHUNK_SIZE_IN_SAMPLES / RATE);
		final int max_frequency_fft = Math.round(MAX_FREQUENCY
				* CHUNK_SIZE_IN_SAMPLES / RATE);
		recorder_.startRecording();
		while (!Thread.interrupted()) {
			short[] audio_data = new short[BUFFER_SIZE_IN_BYTES / 2];
			recorder_.read(audio_data, 0, CHUNK_SIZE_IN_BYTES / 2);
			FreqResult fr = AnalyzeFrequencies(audio_data);
			if(fr!=null)PostToUI(fr.best_frequency);
			else{
				PostToUI(-1);
			}

		}
		recorder_.stop();
	}

	private void PostToUI(final double pitch) {
		handler_.post(new Runnable() {
			public void run() {
				parent_.drawPlayedNote(pitch);
			}
		});
	}

	public FreqResult AnalyzeFrequencies(short[] audio_data) {
		FreqResult fr = new FreqResult();
		double[] x_i = new double[CHUNK_SIZE_IN_SAMPLES];
		for (int i = 0; i < CHUNK_SIZE_IN_SAMPLES; i++) {
			x_i[i] = 0;
		}
		double[] data = new double[CHUNK_SIZE_IN_SAMPLES * 2];
		final int min_frequency_fft = Math.round(MIN_FREQUENCY
				* CHUNK_SIZE_IN_SAMPLES / RATE);
		final int max_frequency_fft = Math.round(MAX_FREQUENCY
				* CHUNK_SIZE_IN_SAMPLES / RATE);

		for (int i = 0; i < CHUNK_SIZE_IN_SAMPLES; i++) {
			data[i] = audio_data[i];

		}
		fft.doFFT(data, x_i, false);

		double best_frequency = min_frequency_fft;
		HashMap<Double, Double> frequencies = new HashMap<Double, Double>();

		// fr.frequencies = new HashMap<Double, Double>();

		double best_amplitude = 0;
		final double draw_frequency_step = 1.0 * RATE / CHUNK_SIZE_IN_SAMPLES;

		List<Double> best_frequencies = new ArrayList<Double>();
		List<Double> best_amps = new ArrayList<Double>();

		for (int i = min_frequency_fft; i <= max_frequency_fft; i++) {

			final double current_frequency = i * 1.0 * RATE
					/ CHUNK_SIZE_IN_SAMPLES;
			final double draw_frequency = Math
					.round((current_frequency - MIN_FREQUENCY)
							/ DRAW_FREQUENCY_STEP)
					* DRAW_FREQUENCY_STEP + MIN_FREQUENCY;

			final double current_amplitude = Math.pow(data[i * 2], 2)
					+ Math.pow(data[i * 2 + 1], 2);

			final double normalized_amplitude = current_amplitude
					* Math.pow(MIN_FREQUENCY * MAX_FREQUENCY, 0.5)
					/ current_frequency;

			Double current_sum_for_this_slot = frequencies.get(draw_frequency);
			if (current_sum_for_this_slot == null) {
				current_sum_for_this_slot = 0.0;
			}

			frequencies.put(draw_frequency, Math.pow(current_amplitude, 0.5)
					/ draw_frequency_step + current_sum_for_this_slot);

			if (normalized_amplitude > best_amplitude) {
				best_frequency = current_frequency;
				best_amplitude = normalized_amplitude;
				best_frequencies.add(current_frequency);
				best_amps.add(best_amplitude);
			}
		}

		List<FrequencyCluster> clusters = new ArrayList<FrequencyCluster>();
		FrequencyCluster currentCluster = new FrequencyCluster();
		clusters.add(currentCluster);
		FrequencyCluster bestCluster = currentCluster;

		if (best_frequencies.size() > 0) {
			currentCluster.add(best_frequencies.get(0), best_amps.get(0));
		}

		// join clusters
		for (int i = 1; i < best_frequencies.size(); i++) {
			double freq = best_frequencies.get(i);
			double amp = best_amps.get(i);

			if (currentCluster.isNear(freq)) {
				currentCluster.add(freq, amp);
				continue;
			}

			currentCluster = new FrequencyCluster();
			clusters.add(currentCluster);
			currentCluster.add(freq, amp);
		}

		// join harmonies
		FrequencyCluster nextCluster;
		for (int i = 1; i < clusters.size(); i++) {
			currentCluster = clusters.get(i - 1);
			nextCluster = clusters.get(i);
			if (currentCluster.isHarmonic(nextCluster.average_frequency)) {
				currentCluster.total_amplitude += nextCluster.total_amplitude;
			}
		}

		best_amplitude = 0;
		best_frequency = 0;
		for (int i = 0; i < clusters.size(); i++) {
			FrequencyCluster clu = clusters.get(i);
			if (best_amplitude < clu.total_amplitude) {
				best_amplitude = clu.total_amplitude;
				best_frequency = clu.average_frequency;
			}
		}
//System.out.println(best_amplitude);
		fr.best_frequency = best_frequency*2;
		fr.frequencies = frequencies;
		if (best_amplitude<MIN_AMPLITUDE){
			return null;
		}
		return fr;
	}

	private SurfaceThread parent_;
	private AudioRecord recorder_;
	private Handler handler_;
}
